# Upgrading Dart's SDK for HTML (blink IDLs).
#
# Typically this is done using the Dart WebCore branch (as it has to be
# staged to get most things working).
#
# Enlist in third_party/WebCore:
#      > cd src/dart/third_party
#      > rm -rf WebCore    (NOTE: Normally detached head using gclient sync)
#      > git clone https://github.com/dart-lang/webcore.git WebCore
#
# To update all *.idl, *.py, LICENSE files, and IDLExtendedAttributes.txt:
#      > cd sdk
#      > python3 tools/dom/scripts/idlsync.py
#
# Display blink files to delete, copy, update, and collisions to review:
#      > python3 tools/dom/scripts/idlsync.py --check
#
# Bring over all blink files to dart/third_party/WebCore (*.py, *.idl, and
# IDLExtendedAttributes.txt):
#      > python3 tools/dom/scripts/idlsync.py
#
# Update the DEPS file SHA for "WebCore_rev" with the committed changes of files
# in WebCore e.g.,   "WebCore_rev": "@NNNNNNNNNNNNNNNNNNNNNNNNN"
#
# Generate the sdk/*.dart files from the new IDLs and PYTHON IDL parsing code
# copied to in dart/third_party/WebCore from src/third_party/WebKit (blink).
#
#      > cd src/dart/tools/dom/script
#      > ./go.sh
#
# Finally, commit the files in dart/third_party/WebCore.

import errno
import optparse
import os.path
import re
import requests
import subprocess
import sys
import time

from shutil import copyfile

# Dart DEPS file checked into the dart-lang/sdk master.
DEPS_GIT = "https://raw.githubusercontent.com/dart-lang/sdk/master/DEPS"
CHROME_TRUNK = "https://chromium.googlesource.com"
WEBKIT_SHA_PATTERN = r'"WebCore_rev": "(\S+)",'

# Chromium remote (GIT repository)
GIT_REMOTES_CHROMIUM = 'https://chromium.googlesource.com/chromium/src.git'

# location of this file
SOURCE_FILE_DIR = 'tools/dom/scripts'

WEBKIT_SOURCE = 'src/third_party/WebKit/Source'
WEBCORE_SOURCE = 'third_party/WebCore'

WEBKIT_BLINK_SOURCE = 'src/third_party/blink'
WEBCORE_BLINK_SOURCE = 'third_party/WebCore/blink'

# Never automatically git add bindings/IDLExtendedAttributes.txt this file has
# been modified by Dart but is usually changed by WebKit blink too.
IDL_EXTENDED_ATTRIBUTES_FILE = 'IDLExtendedAttributes.txt'

# Don't automatically update, delete or add anything in this directory:
#      bindings/dart/scripts
# The scripts in the above directory is the source for our Dart generators that
# is driven from the blink IDL parser AST
DART_SDK_GENERATOR_SCRIPTS = 'bindings/dart/scripts'

# The __init__.py files allow Python to treat directories as packages. Used to
# allow Dart's Python scripts to interact with Chrome's IDL parsing scripts.
PYTHON_INITS = '__init__.py'

# sub directories containing IDLs (core and modules) from the base directory
# src/third_party/WebKit/Source
SUBDIRS = [
    'bindings',
    'core',
    'modules',
]
IDL_EXT = '.idl'
PY_EXT = '.py'
LICENSE_FILE_PREFIX = 'LICENSE'  # e.g., LICENSE-APPLE, etc.

# Look in any file in WebCore we copy from WebKit if this comment is in the file
# then flag this as a special .py or .idl file that needs to be looked at.
DART_CHANGES = ' FIXMEDART: '

# application options passed in.
options = None

warning_messages = []


# Is --dry_run passed in.
def isDryRun():
    global options
    return options['dry_run'] is not None


# Is --verbose passed in.
def isVerbose():
    global options
    return options['verbose'] is not None


# If --WebKit= is specified then compute the directory of the Chromium
# source.
def chromiumDirectory():
    global options
    if options['chromium_dir'] is not None:
        return os.path.expanduser(options['chromium_dir'])
    return os.cwd()


def RunCommand(cmd, valid_exits=[0]):
    """Executes a shell command and return its stdout."""
    if isVerbose():
        print(' '.join(cmd))
    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    output = pipe.communicate()
    if pipe.returncode in valid_exits:
        return output[0]
    else:
        print(output[1])
        print('FAILED. RET_CODE=%d' % pipe.returncode)
        sys.exit(pipe.returncode)


# returns True if // FIXMEDART: is in the file.
def anyDartFixMe(filepath):
    if os.path.exists(filepath):
        data = open(filepath, 'r').read()
        return data.find(DART_CHANGES) != -1
    else:
        return False


# Give a base_dir compute the trailing directory after base_dir
# returns the subpath from base_dir for the path passed in.
def subpath(path, base_dir):
    dir_portion = ''
    head = path
    while True:
        head, tail = os.path.split(head)
        dir_portion = os.path.join(tail, dir_portion)
        if head == base_dir or tail == '':
            break
    return dir_portion


# Copy any file in source_dir (WebKit) to destination_dir (dart/third_party/WebCore)
# source_dir is the src/third_party/WebKit/Source location (blink)
# destination_dir is the src/dart/third_party/WebCore location
# returns idls_copied, py_copied, other_copied
def copy_files(source_dir, src_prefix, destination_dir):
    original_cwd = os.getcwd()
    try:
        os.makedirs(destination_dir)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise
    os.chdir(destination_dir)

    idls = 0  # *.idl files copied
    pys = 0  # *.py files copied
    others = 0  # all other files copied

    for (root, _, files) in os.walk(source_dir, topdown=False):
        dir_portion = subpath(root, source_dir)
        for f in files:
            # Never automatically add any Dart generator scripts (these are the original
            # sources in WebCore) from WebKit to WebCore.
            if dir_portion != DART_SDK_GENERATOR_SCRIPTS:
                if (f.endswith(IDL_EXT) or f == IDL_EXTENDED_ATTRIBUTES_FILE or
                        f.endswith(PY_EXT) or
                        f.startswith(LICENSE_FILE_PREFIX)):
                    if f.endswith(IDL_EXT):
                        idls += 1
                    elif f.endswith(PY_EXT):
                        pys += 1
                    else:
                        others += 1
                    src_file = os.path.join(root, f)

                    # Compute the destination path using sdk/third_party/WebCore
                    subdir_root = src_file[src_file.rfind(src_prefix) +
                                           len(src_prefix):]
                    if subdir_root.startswith(os.path.sep):
                        subdir_root = subdir_root[1:]
                    dst_file = os.path.join(destination_dir, subdir_root)

                    # Need to make src/third_party/WebKit/Source/* to sdk/third_party/WebCore/*

                    destination = os.path.dirname(dst_file)
                    if not os.path.exists(destination):
                        os.makedirs(destination)

                    has_Dart_fix_me = anyDartFixMe(dst_file)

                    if not isDryRun():
                        copyfile(src_file, dst_file)
                    if isVerbose():
                        #print('...copying %s' % os.path.split(dst_file)[1])
                        print('...copying %s' % dst_file)
                    if f == IDL_EXTENDED_ATTRIBUTES_FILE:
                        warning_messages.append(dst_file)
                    else:
                        if has_Dart_fix_me:
                            warning_messages.append(dst_file)
                        if not (isDryRun() or has_Dart_fix_me):
                            # git add the file
                            RunCommand(['git', 'add', dst_file])

    os.chdir(original_cwd)

    return [idls, pys, others]


# Remove any file in webcore_dir that no longer exist in the webkit_dir
# webcore_dir src/dart/third_party/WebCore location
# webkit_dir src/third_party/WebKit/Source location (blink)
# only check if the subdir off from webcore_dir
# return list of files deleted
def remove_obsolete_webcore_files(webcore_dir, webkit_dir, subdir):
    files_to_delete = []

    original_cwd = os.getcwd()

    if os.path.exists(webcore_dir):
        os.chdir(webcore_dir)

        for (root, _, files) in os.walk(
                os.path.join(webcore_dir, subdir), topdown=False):
            dir_portion = subpath(root, webcore_dir)
            for f in files:
                # Never automatically deleted any Dart generator scripts (these are the
                # original sources in WebCore).
                if dir_portion != DART_SDK_GENERATOR_SCRIPTS:
                    check_file = os.path.join(dir_portion, f)
                    check_file_full_path = os.path.join(webkit_dir, check_file)
                    if not os.path.exists(check_file_full_path) and \
                       not(check_file_full_path.endswith(PYTHON_INITS)):
                        if not isDryRun():
                            # Remove the file using git
                            RunCommand(['git', 'rm', check_file])
                        files_to_delete.append(check_file)

    os.chdir(original_cwd)

    return files_to_delete


def ParseOptions():
    parser = optparse.OptionParser()
    parser.add_option(
        '--chromium',
        '-c',
        dest='chromium_dir',
        action='store',
        type='string',
        help='WebKit Chrome directory (e.g., --chromium=~/chrome63',
        default=None)
    parser.add_option(
        '--verbose',
        '-v',
        dest='verbose',
        action='store_true',
        help='Dump all information',
        default=None)
    parser.add_option(
        '--dry_run',
        '-d',
        dest='dry_run',
        action='store_true',
        help='Display results without adding, updating or deleting any files',
        default=None)
    args, _ = parser.parse_args()

    argOptions = {}
    argOptions['chromium_dir'] = args.chromium_dir
    argOptions['verbose'] = args.verbose
    argOptions['dry_run'] = args.dry_run
    return argOptions


# Fetch the DEPS file in src/dart/tools/deps/dartium.deps/DEPS from the GIT repro.
def GetDepsFromGit():
    req = requests.get(DEPS_GIT)
    return req.text


def ValidateGitRemotes():
    #origin  https://chromium.googlesource.com/dart/dartium/src.git (fetch)
    remotes_list = RunCommand(['git', 'remote', '--verbose']).split()
    if (len(remotes_list) > 2 and remotes_list[0] == 'origin' and
            remotes_list[1] == GIT_REMOTES_CHROMIUM):
        return True

    print('ERROR: Unable to find dart/dartium/src repository %s' %
          GIT_REMOTES_CHROMIUM)
    return False


def getChromiumSHA():
    cwd = os.getcwd()
    chromiumDir = chromiumDirectory()

    webkit_dir = os.path.join(chromiumDir, WEBKIT_SOURCE)
    os.chdir(webkit_dir)

    if ValidateGitRemotes():
        chromium_sha = RunCommand(['git', 'log', '--format=format:%H', '-1'])
    else:
        chromium_sha = -1

    os.chdir(cwd)
    return chromium_sha


def getCurrentDartSHA():
    cwd = os.getcwd()

    if cwd.endswith('dart'):
        # In src/dart
        src_dir, _ = os.path.split(cwd)
    elif cwd.endswith('sdk'):
        src_dir = cwd
    else:
        src_dir = os.path.join(cwd, 'sdk')
    os.chdir(src_dir)

    if ValidateGitRemotes():
        dart_sha = RunCommand(['git', 'log', '--format=format:%H', '-1'])
    else:
        dart_sha = -1

    os.chdir(cwd)
    return dart_sha


# Returns the SHA of the Dartium/Chromium in the DEPS file.
def GetDEPSWebCoreGitRevision(deps, component):
    """Returns a tuple with the (dartium chromium repo, latest revision)."""
    foundIt = re.search(WEBKIT_SHA_PATTERN, deps)
    #url_base, url_pattern = DEPS_PATTERNS[component]
    #url = url_base + re.search(url_pattern, deps).group(1)
    # Get the SHA for the Chromium/WebKit changes for Dartium.
    #revision = url[len(url_base):]
    revision = foundIt.group(1)[1:]
    print('%s' % revision)
    return revision


def copy_subdir(src, src_prefix, dest, subdir):
    idls_deleted = remove_obsolete_webcore_files(dest, src, subdir)
    print("%s files removed in WebCore %s" % (idls_deleted.__len__(), subdir))
    if isVerbose():
        for delete_file in idls_deleted:
            print("    %s" % delete_file)

    idls_copied, py_copied, other_copied = copy_files(
        os.path.join(src, subdir), src_prefix, dest)
    if idls_copied > 0:
        print("Copied %s IDLs to %s" % (idls_copied, subdir))
    if py_copied > 0:
        print("Copied %s PYs to %s" % (py_copied, subdir))
    if other_copied > 0:
        print("Copied %s other to %s\n" % (other_copied, subdir))


def main():
    global options
    options = ParseOptions()

    current_dir = os.path.dirname(os.path.abspath(__file__))
    if not current_dir.endswith(SOURCE_FILE_DIR):
        print('ERROR: idlsync.py not run in proper directory (%s)\n',
              current_dir)

    base_directory = current_dir[:current_dir.rfind(SOURCE_FILE_DIR)]

    # Validate DEPS WebCore_rev SHA DOES NOT match the SHA of chromium master.
    deps = GetDepsFromGit()
    webcore_revision = GetDEPSWebCoreGitRevision(deps, 'webkit')
    chromium_sha = getChromiumSHA()
    if webcore_revision == chromium_sha:
        print("ERROR: Nothing to update in WebCore, WebCore_rev SHA in DEPS "
              "matches Chromium GIT master SHA in %s" % options['webkit_dir'])
        return

    start_time = time.time()

    # Copy scripts from third_party/blink/tools to third_party/WebCore/blink/tools
    #
    # This also implies that the files:
    #     WebCore/bindings/scripts/code_generator_web_agent_api.py
    #     WebCore/bindings/scripts/utilities.py
    #
    # Need to have sys.path.append at beginning of the above files changed from:
    #
    #    sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..',
    #                                 'third_party', 'blink', 'tools'))
    # to
    #
    #    sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..',
    #                                 'blink', 'tools'))
    #
    webkit_blink_dir = os.path.join(chromiumDirectory(), WEBKIT_BLINK_SOURCE)
    webcore_blink_dir = os.path.join(base_directory, WEBCORE_BLINK_SOURCE)
    copy_subdir(webkit_blink_dir, WEBKIT_BLINK_SOURCE, webcore_blink_dir, "")

    chromium_webkit_dir = os.path.join(chromiumDirectory(), WEBKIT_SOURCE)
    dart_webcore_dir = os.path.join(base_directory, WEBCORE_SOURCE)
    for subdir in SUBDIRS:
        copy_subdir(chromium_webkit_dir, WEBKIT_SOURCE, dart_webcore_dir,
                    subdir)

    end_time = time.time()

    print(
        'WARNING: File(s) contain FIXMEDART and are NOT "git add " please review:'
    )
    for warning in warning_messages:
        print('    %s' % warning)

    print('\nDone idlsync completed in %s seconds' %
          round(end_time - start_time, 2))


if __name__ == '__main__':
    sys.exit(main())
